EDA: exploración y procesamiento de imágenes#

Librerías necesarias#

import os
import cv2
import json
import shutil
import imageio
import warnings
import nbimporter
import pandas as pd
import seaborn as sns
from PIL import Image
import matplotlib.pyplot as plt
from funciones import *
warnings.filterwarnings("ignore")
sns.set(style="whitegrid")
plt.rcParams['font.family'] = 'Bahnschrift'
color_morado = '#9370DB'
colores_morado = ['#9370DB', '#8A2BE2']
ruta_imagenes = "C:/Users/kamac/OneDrive/Desktop/Parcial1_DeepLearning/data/train_filter/"
imagen_pruebas = "0006ff2aa7cd"
imagen_pruebas2 = "912ebd02ee50"

Contextualización de los datos#

Los datos incluyen imágenes histológicas de riñón humano en formato WSI, de las cuales se han extraído pequeños mosaicos llamados teselas.

El objetivo es identificar y segmentar estructuras de microvasculatura (vasos sanguíneos) en estas imágenes. Para esto, los datos se dividen en tres conjuntos principales con diferentes niveles de anotación:

  • Conjunto de Datos 1: Teselas con anotaciones revisadas por expertos.

  • Conjunto de Datos 2: Teselas con anotaciones dispersas, pero sin revisión experta.

  • Conjunto de Datos 3: Teselas de WSI adicionales sin anotaciones (En este proyecto no se usará este conjunto de datos ya que es ideal para posibles técnicas de aprendizaje semisupervisado o autosupervisado y nuestro enfoque es en modelos de aprendizaje supervisado).

¿Qué es una WSI y qué son las teselas?

Las WSI (Whole Slide Images) son imágenes digitales de alta resolución obtenidas de muestras de tejido en un portaobjetos de microscopio. Debido a su gran tamaño, estas imágenes se dividen en pequeñas secciones llamadas teselas, que son imágenes más manejables de 512x512 píxeles. Las teselas permiten analizar regiones específicas del tejido sin necesidad de procesar la WSI completa.

Archivos y estructura de datos#

Carpetas principales:#

  • train: Contiene las imágenes TIFF de los mosaicos de entrenamiento (tamaño 512x512 píxeles).

Archivos clave:#

  • polygons.jsonl (Máscaras de segmentación poligonal)

    • Este archivo contiene las anotaciones de segmentación en formato JSONL (JSON Lines, es decir, un JSON por línea).

    • Cada línea corresponde a una imagen y contiene:

      • id: Identificador de la imagen.

      • annotations: Lista de anotaciones de estructuras con:

        • type: Tipo de estructura segmentada:

          • blood_vessel (vaso sanguíneo, el objetivo principal).

          • glomerulus (glomerulo, una estructura que no debe incluirse en las predicciones, ya que se considerará un error).

          • unsure (estructuras que los anotadores no pudieron clasificar con certeza).

        • coordinates: Lista de coordenadas poligonales que definen la máscara de segmentación.

  • tile_meta.csv (Metadatos de cada tesela)

    • Contiene información sobre cada mosaico, incluyendo:

      • source_wsi: De qué WSI proviene.

      • {i, j}: Ubicación dentro de la WSI de donde se extrajo.

      • dataset: A qué conjunto de datos pertenece (1, 2 o 3).

  • wsi_meta.csv (Metadatos de las imágenes WSI completas)

    • Contiene información demográfica del donante del tejido:

      • age, sex, race, height, weight, bmi.

¿Qué es una imagen TIFF?

TIFF (Tagged Image File Format) es un formato de imagen de alta calidad y sin pérdida de datos. Se utiliza comúnmente en microscopía, impresión y almacenamiento de imágenes médicas y científicas.

Los datos utilizados en este trabajo provienen de la competencia HuBMAP - Hacking the Human Vasculature [Howard et al., 2023], disponibles en Kaggle.

wsi_meta: Datos demográficos de los donantes de tejidos#

En este estudio participaron únicamente cuatro pacientes, a quienes se les tomaron muestras de tejido en portaobjetos de microscopio. La cantidad limitada de participantes se debió al uso de imágenes WSI de alta resolución. Estas imágenes, debido a su gran tamaño, tuvieron que ser divididas en pequeñas secciones llamadas teselas. Este proceso de división fue necesario para manejar adecuadamente los datos, pero también representó una limitación en el número de pacientes que se pudo incluir en el estudio. Veamos un poco sobre estos pacientes.

data_donantes = pd.read_csv('wsi_meta.csv')
data_donantes
source_wsi age sex race height weight bmi
0 1 58 F W 160.0 59.0 23.0
1 2 56 F W 175.2 139.6 45.5
2 3 73 F W 162.3 87.5 33.2
3 4 53 M B 166.0 73.0 26.5
plt.figure(figsize=(12, 8))
plt.subplot(2, 2, 1)
sns.histplot(data_donantes['age'], bins = 10, kde = True, color = color_morado)
plt.title('Distribución de Edad', fontsize = 14)
plt.xlabel('Edad', fontsize = 12)
plt.ylabel('Frecuencia', fontsize = 12)
plt.grid(axis='y', linestyle='--', alpha = 0.6)
plt.tight_layout()
plt.show()
_images/ba538320735aff62f6163ecc9facecbeb9b306ec3bca77433e31299de8edb45e.png

Al observar la gráfica notamos que no existe una distribución clara en la figura dado la cantidad de pacientes a las que se le realizó el estudio y cada uno de estos pacientes tiene una edad diferente.

plt.figure(figsize = (10, 6))
sex_counts = data_donantes['sex'].value_counts()
plt.pie(sex_counts, labels=sex_counts.index, autopct = '%1.1f%%', startangle = 140, colors = colores_morado)
plt.title('Distribución por Sexo', fontsize = 14)
plt.show()
_images/bf1dbdad62e36cb5d219c1ecd82b3c565b176cd16e6854b0f7e497575ff5bf88.png

De lo anterior podemos concluir que el sexo que predomina entre los pacientes muestreados es femenino, lo que corresponde aproximadamente a 3 mujeres.

plt.figure(figsize=(10, 6))
sns.boxplot(x='bmi', data = data_donantes, color = color_morado)
plt.title('Distribución del Índice de masa corporal de los pacientes', fontsize = 14)
plt.xlabel('IMC', fontsize = 12)
plt.show()
_images/c2537bfe28692914f791ceb029038e0ed762a2a50d71a8be1ab59c917a81f931.png

El boxplot muestra la distribución del IMC de cuatro pacientes, con valores entre aproximadamente 23 y 45. La mediana se encuentra alrededor de 30-32, lo que indica que dos pacientes tienen un IMC inferior y dos superior a este valor. La caja abarca el rango intercuartílico (~27-35), sugiriendo que la mitad de los valores están en este intervalo. Los bigotes alcanzan los extremos sin valores atípicos visibles, reflejando una distribución sin datos fuera del rango esperado. En general, la mayoría de los pacientes presentan sobrepeso u obesidad, ya que el IMC se encuentra mayormente por encima de 25, lo que podría implicar riesgos para la salud asociados a estas condiciones.

tile_meta: Datos sobre las teselas#

En el siguiente conjunto de datos, se presenta información detallada sobre las muestras divididas en teselas.

data_info_teselas = pd.read_csv('tile_meta.csv')
data_info_teselas.head()
id source_wsi dataset i j
0 0006ff2aa7cd 2 2 16896 16420
1 000e79e206b7 6 3 10240 29184
2 00168d1b7522 2 2 14848 14884
3 00176a88fdb0 7 3 14848 25088
4 0033bbc76b6b 1 1 10240 43008
data_info_teselas.shape
(7033, 5)

El conjunto de datos consta de 7033 observaciones, es decir, 7033 teselas. Dado que se decidió no trabajar con el conjunto de datos 3, filtraremos el conjunto para incluir únicamente los datos pertenecientes a los conjuntos 1 y 2.

data_imagenes_teselas = data_info_teselas[data_info_teselas['dataset'].isin([1, 2])]
data_imagenes_teselas.shape
(1633, 5)

Ahora solo contamos con 1633 imágenes, que son las de interés en este trabajo. Veamos algunas gráficas para estos datos.

Guardemos las imágenes filtradas en una nueva carpeta

def copiar_imagenes_a_nueva_carpeta(df, columna_id, ruta_origen, ruta_destino):

    # Crear la carpeta destino si no existe
    if not os.path.exists(ruta_destino):
        os.makedirs(ruta_destino)

    # Iterar sobre los IDs y copiar las imágenes
    for img_id in df[columna_id]:
        nombre_archivo = f"{img_id}.tif"  # Suponiendo que los archivos tienen extensión .tif
        ruta_original = os.path.join(ruta_origen, nombre_archivo)
        ruta_copia = os.path.join(ruta_destino, nombre_archivo)

        if os.path.exists(ruta_original):
            shutil.copy(ruta_original, ruta_copia)  # Copiar la imagen
        else:
            print(f"⚠️ Imagen no encontrada: {nombre_archivo}")


data_imagenes_teselas = data_info_teselas[data_info_teselas['dataset'].isin([1, 2])]

ruta_imagenes = "C:/Users/kamac/OneDrive/Desktop/Parcial1_DeepLearning/data/train/" 
ruta_destino = "C:/Users/kamac/OneDrive/Desktop/Parcial1_DeepLearning/data/train_filter/" 

copiar_imagenes_a_nueva_carpeta(data_imagenes_teselas, "id", ruta_imagenes, ruta_destino)
dataset_counts = data_imagenes_teselas['dataset'].value_counts().sort_index()
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
axes[0].pie(dataset_counts, labels=dataset_counts.index, autopct='%1.1f%%', startangle=140, colors=colores_morado)
axes[0].set_title('Cantidad de imágenes por conjunto de datos', fontsize=14)
sns.barplot(x=dataset_counts.index, y=dataset_counts.values, ax=axes[1], palette=colores_morado)
axes[1].set_title('Cantidad de imágenes por conjunto de datos', fontsize=14)
axes[1].set_xlabel('Conjunto de datos', fontsize=12)
axes[1].set_ylabel('Cantidad', fontsize=12)
for i, count in enumerate(dataset_counts.values):
    axes[1].text(i, count + 0.5, str(count), ha='center', va='bottom', fontsize=10)

plt.tight_layout()
plt.show()
_images/e2dad49a4ed77c181c80348cba0fb224a36795ab8c99f82eebc499577a716555.png

Notamos que el conjunto de datos más predominante es el Conjunto de Datos 2, el cual corresponde a teselas con anotaciones dispersas, pero sin revisión experta. Esto no es ideal, sin embargo esto se podría comparar con las imágenes del Conjunto de Datos 1 las cuales incluyen teselas con anotaciones revisadas por expertos.

dataset_counts2 = data_imagenes_teselas['source_wsi'].value_counts().sort_index()
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
axes[0].pie(dataset_counts2, labels=dataset_counts2.index, autopct='%1.1f%%', startangle=140, colors=colores_morado)
axes[0].set_title('Cantidad de imágenes por tipo de WSI', fontsize=14)
sns.barplot(x=dataset_counts2.index, y=dataset_counts2.values, ax=axes[1], palette=colores_morado)
axes[1].set_title('Cantidad de imágenes por tipo de WSI', fontsize=14)
axes[1].set_xlabel('WSI', fontsize=12)
axes[1].set_ylabel('Cantidad', fontsize=12)
for i, count in enumerate(dataset_counts2.values):
    axes[1].text(i, count + 0.5, str(count), ha='center', va='bottom', fontsize=10)

plt.tight_layout()
plt.show()
_images/857d96c82c0c75893f772783b33d234a3cda4d361c5960c2ad9fbf203931f215.png

En este estudio, trabajamos con imágenes WSI (Whole Slide Images), que son imágenes digitales de alta resolución obtenidas de muestras de tejido en portaobjetos de microscopio. Aunque se capturaron más WSI, solo consideramos cuatro en este análisis, excluyendo aquellas asignadas al conjunto de datos 3.

Al observar las gráficas, notamos que las frecuencias de las imágenes por WSI son relativamente similares, sin diferencias extremadamente marcadas. Sin embargo, hay una tendencia clara: la WSI 1 contiene la mayor cantidad de imágenes, seguida por la 2, y en menor proporción la 3 y la 4. Esto sugiere que, a medida que aumentó el número de WSI, la cantidad de imágenes extraídas de cada una disminuyó, posiblemente debido a criterios de muestreo o a la disponibilidad de regiones de interés en cada WSI.

data_imagenes_teselas['id'].unique(). shape[0]
1633

Importante: Nótese que todas las imágenes tienen un id único.

polygons.jsonl: Máscaras de segmentación poligonal#

Ahora, exploremos el archivo .json. Este archivo contiene las máscaras de segmentación poligonal para los conjuntos de datos 1 y 2. Incluye tanto máscaras revisadas por expertos como aquellas que no lo han sido. Profundicemos un poco más en esta información.

# Cargar las anotaciones desde el archivo JSONL
with open('polygons.jsonl') as archivo_json:  # Abre el archivo JSONL en modo lectura
    lista_json = list(archivo_json)  # Lee todas las líneas y las almacena en una lista

# Inicializar una lista para almacenar los datos procesados
mascaras = []

# Convertir cada línea de JSON en un diccionario y almacenarlo en la lista
for linea_json in lista_json:
    mascaras.append(json.loads(linea_json))  # Convierte la línea JSON en un diccionario y lo agrega a la lista

# Ver el total de filas
total_filas = len(mascaras)
print(f"Total de filas: {total_filas}")
Total de filas: 1633

Observamos que el total de filas (máscaras de segmentación) es de 1633, coincidiendo con el número obtenido previamente al filtrar las teselas para los conjuntos de datos 1 y 2. Esta coincidencia es crucial para poder aplicar posteriormente los modelos correspondientes.

Veamos un ejemplo de ¿Cómo se ve una fila en cada línea del archivo JSON

print("Ejemplo de máscara:", mascaras[:1])  
Ejemplo de máscara: [{'id': '0006ff2aa7cd', 'annotations': [{'type': 'glomerulus', 'coordinates': [[[167, 249], [166, 249], [165, 249], [164, 249], [163, 249], [162, 249], [161, 249], [160, 249], [159, 249], [158, 249], [157, 249], [156, 249], [155, 249], [154, 249], [153, 249], [152, 249], [151, 249], [150, 249], [149, 249], [148, 249], [147, 249], [146, 249], [145, 249], [144, 249], [143, 249], [142, 249], [141, 249], [140, 249], [139, 249], [138, 249], [137, 249], [136, 249], [135, 249], [134, 249], [133, 249], [132, 249], [131, 249], [130, 249], [129, 249], [128, 249], [127, 249], [126, 249], [125, 249], [124, 249], [123, 249], [122, 249], [122, 248], [121, 248], [120, 248], [119, 248], [118, 248], [117, 248], [117, 247], [116, 247], [115, 247], [115, 246], [114, 246], [113, 246], [112, 246], [111, 246], [110, 246], [110, 245], [109, 245], [108, 245], [107, 245], [106, 245], [106, 244], [105, 244], [105, 243], [104, 243], [103, 243], [103, 242], [102, 242], [101, 242], [100, 242], [99, 242], [99, 241], [98, 241], [98, 240], [97, 240], [97, 239], [96, 239], [96, 238], [95, 238], [95, 237], [94, 237], [93, 237], [93, 236], [92, 236], [91, 236], [91, 235], [90, 235], [89, 235], [89, 234], [88, 234], [88, 233], [87, 233], [87, 232], [86, 232], [86, 231], [85, 231], [85, 230], [84, 230], [83, 230], [83, 229], [82, 229], [82, 228], [81, 228], [81, 227], [80, 227], [80, 226], [79, 226], [79, 225], [79, 224], [78, 224], [78, 223], [77, 223], [77, 222], [76, 222], [76, 221], [76, 220], [75, 220], [75, 219], [74, 219], [74, 218], [73, 218], [73, 217], [73, 216], [72, 216], [72, 215], [72, 214], [71, 214], [71, 213], [71, 212], [70, 212], [70, 211], [69, 211], [69, 210], [69, 209], [68, 209], [68, 208], [67, 208], [67, 207], [67, 206], [66, 206], [66, 205], [66, 204], [65, 204], [65, 203], [65, 202], [64, 202], [64, 201], [63, 201], [63, 200], [63, 199], [63, 198], [62, 198], [62, 197], [62, 196], [61, 196], [61, 195], [60, 195], [60, 194], [60, 193], [60, 192], [59, 192], [59, 191], [58, 191], [58, 190], [57, 190], [57, 189], [57, 188], [56, 188], [56, 187], [55, 187], [55, 186], [55, 185], [54, 185], [54, 184], [53, 184], [53, 183], [53, 182], [52, 182], [52, 181], [51, 181], [51, 180], [51, 179], [50, 179], [50, 178], [49, 178], [49, 177], [48, 177], [48, 176], [48, 175], [47, 175], [47, 174], [47, 173], [46, 173], [46, 172], [45, 172], [45, 171], [44, 171], [44, 170], [43, 170], [43, 169], [42, 169], [42, 168], [41, 168], [41, 167], [41, 166], [40, 166], [40, 165], [39, 165], [39, 164], [38, 164], [38, 163], [38, 162], [37, 162], [37, 161], [37, 160], [36, 160], [36, 159], [35, 159], [35, 158], [34, 158], [34, 157], [34, 156], [33, 156], [33, 155], [32, 155], [32, 154], [32, 153], [31, 153], [31, 152], [30, 152], [30, 151], [29, 151], [29, 150], [29, 149], [28, 149], [28, 148], [28, 147], [27, 147], [27, 146], [26, 146], [26, 145], [25, 145], [25, 144], [25, 143], [24, 143], [24, 142], [24, 141], [23, 141], [23, 140], [22, 140], [22, 139], [21, 139], [21, 138], [21, 137], [20, 137], [20, 136], [19, 136], [19, 135], [18, 135], [18, 134], [18, 133], [17, 133], [16, 133], [16, 132], [16, 131], [15, 131], [15, 130], [15, 129], [15, 128], [14, 128], [14, 127], [13, 127], [13, 126], [12, 126], [12, 125], [12, 124], [11, 124], [11, 123], [11, 122], [10, 122], [10, 121], [9, 121], [9, 120], [9, 119], [8, 119], [8, 118], [8, 117], [7, 117], [7, 116], [6, 116], [6, 115], [5, 115], [5, 114], [5, 113], [4, 113], [4, 112], [3, 112], [3, 111], [3, 110], [2, 110], [2, 109], [2, 108], [2, 107], [2, 106], [1, 106], [1, 105], [0, 105], [0, 104], [0, 103], [0, 102], [0, 101], [0, 100], [0, 99], [0, 98], [0, 97], [0, 96], [0, 95], [0, 94], [0, 93], [0, 92], [0, 91], [0, 90], [0, 89], [0, 88], [0, 87], [0, 86], [0, 85], [0, 84], [0, 83], [0, 82], [0, 81], [0, 80], [0, 79], [0, 78], [0, 77], [0, 76], [0, 75], [0, 74], [0, 73], [0, 72], [0, 71], [0, 70], [0, 69], [0, 68], [0, 67], [0, 66], [0, 65], [0, 64], [0, 63], [0, 62], [0, 61], [0, 60], [0, 59], [0, 58], [0, 57], [0, 56], [0, 55], [0, 54], [0, 53], [0, 52], [0, 51], [0, 50], [0, 49], [0, 48], [0, 47], [0, 46], [0, 45], [0, 44], [0, 43], [0, 42], [0, 41], [0, 40], [0, 39], [0, 38], [0, 37], [0, 36], [0, 35], [0, 34], [0, 33], [0, 32], [0, 31], [0, 30], [0, 29], [0, 28], [0, 27], [0, 26], [0, 25], [0, 24], [0, 23], [0, 22], [0, 21], [0, 20], [0, 19], [0, 18], [0, 17], [0, 16], [0, 15], [0, 14], [0, 13], [0, 12], [0, 11], [0, 10], [0, 9], [0, 8], [0, 7], [0, 6], [0, 5], [0, 4], [0, 3], [0, 2], [0, 1], [1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], [8, 0], [9, 0], [10, 0], [11, 0], [12, 0], [13, 0], [14, 0], [15, 0], [16, 0], [17, 0], [18, 0], [19, 0], [20, 0], [21, 0], [22, 0], [23, 0], [24, 0], [25, 0], [26, 0], [27, 0], [28, 0], [29, 0], [30, 0], [31, 0], [32, 0], [33, 0], [34, 0], [35, 0], [36, 0], [37, 0], [38, 0], [39, 0], [40, 0], [41, 0], [42, 0], [43, 0], [44, 0], [45, 0], [46, 0], [47, 0], [48, 0], [49, 0], [50, 0], [51, 0], [52, 0], [53, 0], [54, 0], [55, 0], [56, 0], [57, 0], [58, 0], [59, 0], [60, 0], [61, 0], [62, 0], [63, 0], [64, 0], [65, 0], [66, 0], [67, 0], [68, 0], [69, 0], [70, 0], [71, 0], [72, 0], [73, 0], [74, 0], [75, 0], [76, 0], [77, 0], [78, 0], [79, 0], [80, 0], [81, 0], [82, 0], [83, 0], [84, 0], [85, 0], [86, 0], [87, 0], [88, 0], [89, 0], [90, 0], [91, 0], [92, 0], [93, 0], [94, 0], [95, 0], [96, 0], [97, 0], [98, 0], [99, 0], [100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0], [108, 0], [109, 0], [110, 0], [111, 0], [112, 0], [113, 0], [114, 0], [115, 0], [116, 0], [117, 0], [118, 0], [119, 0], [120, 0], [121, 0], [122, 0], [123, 0], [124, 0], [125, 0], [126, 0], [127, 0], [128, 0], [129, 0], [130, 0], [131, 0], [132, 0], [133, 0], [134, 0], [135, 0], [136, 0], [137, 0], [138, 0], [139, 0], [140, 0], [141, 0], [142, 0], [143, 0], [144, 0], [145, 0], [146, 0], [147, 0], [148, 0], [149, 0], [150, 0], [151, 0], [152, 0], [153, 0], [154, 0], [155, 0], [156, 0], [157, 0], [158, 0], [159, 0], [160, 0], [161, 0], [162, 0], [163, 0], [164, 0], [165, 0], [166, 0], [167, 0], [168, 0], [169, 0], [170, 0], [171, 0], [172, 0], [173, 0], [174, 0], [175, 0], [176, 0], [177, 0], [178, 0], [179, 0], [180, 0], [181, 0], [182, 0], [183, 0], [184, 0], [185, 0], [186, 0], [187, 0], [188, 0], [189, 0], [190, 0], [191, 0], [192, 0], [193, 0], [194, 0], [195, 0], [196, 0], [197, 0], [198, 0], [199, 0], [200, 0], [201, 0], [202, 0], [203, 0], [204, 0], [205, 0], [206, 0], [207, 0], [208, 0], [209, 0], [210, 0], [211, 0], [212, 0], [213, 0], [214, 0], [215, 0], [216, 0], [217, 0], [218, 0], [219, 0], [220, 0], [221, 0], [222, 0], [223, 0], [224, 0], [225, 0], [226, 0], [227, 0], [228, 0], [229, 0], [230, 0], [231, 0], [232, 0], [233, 0], [234, 0], [235, 0], [236, 0], [237, 0], [238, 0], [239, 0], [240, 0], [241, 0], [242, 0], [243, 0], [244, 0], [245, 0], [246, 0], [247, 0], [248, 0], [249, 0], [250, 0], [251, 0], [252, 0], [253, 0], [253, 1], [253, 2], [253, 3], [253, 4], [253, 5], [253, 6], [253, 7], [253, 8], [253, 9], [253, 10], [253, 11], [253, 12], [254, 12], [254, 13], [254, 14], [254, 15], [255, 15], [255, 16], [255, 17], [256, 17], [256, 18], [256, 19], [256, 20], [256, 21], [256, 22], [256, 23], [256, 24], [256, 25], [256, 26], [256, 27], [256, 28], [257, 28], [257, 29], [257, 30], [257, 31], [258, 31], [258, 32], [258, 33], [258, 34], [259, 34], [259, 35], [259, 36], [259, 37], [259, 38], [259, 39], [259, 40], [259, 41], [259, 42], [259, 43], [259, 44], [259, 45], [259, 46], [260, 46], [260, 47], [260, 48], [260, 49], [261, 49], [261, 50], [261, 51], [262, 51], [262, 52], [262, 53], [262, 54], [262, 55], [262, 56], [262, 57], [262, 58], [262, 59], [262, 60], [263, 60], [263, 61], [263, 62], [263, 63], [263, 64], [263, 65], [263, 66], [264, 66], [264, 67], [264, 68], [264, 69], [265, 69], [265, 70], [265, 71], [265, 72], [265, 73], [265, 74], [265, 75], [265, 76], [266, 76], [266, 77], [266, 78], [266, 79], [266, 80], [266, 81], [266, 82], [266, 83], [266, 84], [266, 85], [266, 86], [266, 87], [266, 88], [266, 89], [266, 90], [267, 90], [267, 91], [267, 92], [268, 92], [268, 93], [268, 94], [268, 95], [269, 95], [269, 96], [269, 97], [269, 98], [269, 99], [269, 100], [269, 101], [269, 102], [269, 103], [269, 104], [269, 105], [269, 106], [269, 107], [269, 108], [269, 109], [269, 110], [269, 111], [269, 112], [269, 113], [269, 114], [269, 115], [270, 115], [270, 116], [270, 117], [270, 118], [270, 119], [270, 120], [270, 121], [270, 122], [270, 123], [270, 124], [270, 125], [270, 126], [270, 127], [270, 128], [270, 129], [270, 130], [270, 131], [270, 132], [270, 133], [270, 134], [270, 135], [270, 136], [270, 137], [270, 137], [269, 138], [269, 139], [269, 140], [269, 141], [269, 142], [269, 143], [269, 144], [269, 145], [269, 146], [269, 147], [269, 148], [269, 149], [269, 150], [269, 150], [268, 151], [268, 152], [268, 153], [268, 153], [267, 154], [267, 155], [267, 155], [266, 156], [266, 157], [266, 158], [266, 159], [266, 160], [266, 160], [265, 161], [265, 162], [265, 163], [265, 164], [265, 165], [265, 165], [264, 166], [264, 166], [263, 167], [263, 168], [263, 169], [263, 169], [262, 170], [262, 171], [262, 171], [261, 172], [261, 173], [261, 173], [260, 174], [260, 175], [260, 176], [260, 176], [259, 177], [259, 178], [259, 178], [258, 179], [258, 180], [258, 180], [257, 181], [257, 182], [257, 182], [256, 183], [256, 184], [256, 184], [255, 185], [255, 185], [254, 186], [254, 187], [254, 187], [253, 188], [253, 189], [253, 189], [252, 190], [252, 191], [252, 191], [251, 191], [250, 192], [250, 192], [249, 193], [249, 193], [248, 194], [248, 194], [247, 195], [247, 195], [246, 196], [246, 196], [245, 197], [245, 197], [244, 198], [244, 198], [243, 199], [243, 200], [243, 200], [242, 201], [242, 201], [241, 202], [241, 203], [241, 203], [240, 204], [240, 204], [239, 205], [239, 206], [239, 206], [238, 207], [238, 207], [237, 207], [236, 208], [236, 209], [236, 209], [235, 210], [235, 210], [234, 211], [234, 211], [233, 212], [233, 212], [232, 213], [232, 213], [231, 214], [231, 214], [230, 215], [230, 215], [229, 216], [229, 216], [228, 217], [228, 217], [227, 217], [226, 218], [226, 218], [225, 219], [225, 219], [224, 220], [224, 220], [223, 220], [222, 221], [222, 221], [221, 222], [221, 222], [220, 223], [220, 223], [219, 223], [218, 224], [218, 224], [217, 224], [216, 225], [216, 225], [215, 226], [215, 226], [214, 227], [214, 227], [213, 228], [213, 228], [212, 228], [211, 229], [211, 229], [210, 230], [210, 230], [209, 230], [208, 230], [207, 231], [207, 231], [206, 232], [206, 232], [205, 232], [204, 233], [204, 233], [203, 234], [203, 234], [202, 235], [202, 235], [201, 236], [201, 236], [200, 236], [199, 236], [198, 236], [197, 237], [197, 237], [196, 238], [196, 238], [195, 239], [195, 239], [194, 239], [193, 240], [193, 240], [192, 241], [192, 241], [191, 242], [191, 242], [190, 242], [189, 242], [188, 242], [187, 243], [187, 243], [186, 243], [185, 244], [185, 244], [184, 245], [184, 245], [183, 245], [182, 245], [181, 246], [181, 246], [180, 246], [179, 246], [178, 246], [177, 246], [176, 247], [176, 247], [175, 248], [175, 248], [174, 248], [173, 248], [172, 248], [171, 248], [170, 248], [169, 248], [168, 248], [167, 249], [167, 249]]]}, {'type': 'blood_vessel', 'coordinates': [[[283, 109], [282, 109], [281, 109], [280, 109], [279, 109], [279, 108], [278, 108], [277, 108], [277, 107], [276, 107], [276, 106], [276, 105], [275, 105], [275, 104], [275, 103], [275, 102], [274, 102], [274, 101], [274, 100], [274, 99], [273, 99], [273, 98], [273, 97], [273, 96], [273, 95], [273, 94], [272, 94], [272, 93], [272, 92], [272, 91], [272, 90], [272, 89], [272, 88], [272, 87], [272, 86], [272, 85], [273, 84], [273, 84], [273, 83], [274, 82], [274, 82], [274, 81], [275, 80], [275, 80], [275, 79], [276, 78], [276, 78], [276, 77], [277, 76], [277, 76], [277, 75], [278, 74], [279, 74], [279, 74], [280, 73], [281, 73], [282, 73], [282, 74], [283, 74], [284, 74], [284, 75], [284, 76], [285, 76], [286, 76], [286, 77], [286, 78], [287, 78], [287, 79], [287, 80], [288, 80], [288, 81], [288, 82], [289, 82], [290, 82], [290, 83], [290, 84], [291, 84], [291, 85], [291, 86], [291, 87], [291, 88], [291, 89], [291, 90], [291, 91], [291, 92], [291, 93], [291, 94], [291, 95], [291, 96], [291, 97], [291, 98], [291, 99], [291, 99], [290, 100], [290, 101], [290, 101], [289, 102], [289, 102], [288, 103], [288, 104], [288, 104], [287, 105], [287, 106], [287, 106], [286, 107], [286, 107], [285, 108], [285, 108], [284, 108], [283, 109], [283, 109]]]}, {'type': 'blood_vessel', 'coordinates': [[[104, 292], [103, 292], [102, 292], [101, 292], [101, 291], [100, 291], [99, 291], [99, 290], [98, 290], [98, 289], [97, 289], [97, 288], [97, 287], [96, 287], [96, 286], [96, 285], [95, 285], [95, 284], [94, 284], [94, 283], [94, 282], [93, 282], [93, 281], [92, 281], [92, 280], [92, 279], [91, 279], [91, 278], [90, 278], [90, 277], [89, 277], [89, 276], [88, 276], [88, 275], [87, 275], [87, 274], [87, 273], [86, 273], [85, 273], [85, 272], [84, 272], [84, 271], [84, 270], [83, 270], [83, 269], [83, 268], [82, 268], [82, 267], [82, 266], [81, 266], [81, 265], [81, 264], [81, 263], [80, 263], [80, 262], [80, 261], [79, 261], [79, 260], [79, 259], [79, 258], [78, 258], [78, 257], [78, 256], [77, 256], [77, 255], [77, 254], [77, 253], [76, 253], [76, 252], [75, 252], [75, 251], [75, 250], [74, 250], [74, 249], [74, 248], [73, 248], [73, 247], [73, 246], [72, 246], [72, 245], [72, 244], [72, 243], [73, 242], [73, 242], [73, 241], [74, 240], [74, 240], [75, 239], [76, 239], [76, 239], [77, 238], [78, 238], [79, 238], [79, 239], [80, 239], [81, 239], [81, 240], [82, 240], [82, 241], [82, 242], [83, 242], [83, 243], [84, 243], [84, 244], [84, 245], [85, 245], [85, 246], [86, 246], [86, 247], [87, 247], [87, 248], [88, 248], [88, 249], [89, 249], [90, 249], [90, 250], [91, 250], [91, 251], [92, 251], [92, 252], [93, 252], [94, 252], [94, 253], [95, 253], [96, 253], [96, 254], [97, 254], [98, 254], [99, 254], [99, 255], [100, 255], [101, 255], [101, 256], [102, 256], [102, 257], [103, 257], [104, 257], [104, 258], [105, 258], [105, 259], [105, 260], [106, 260], [106, 261], [106, 262], [106, 263], [106, 264], [107, 264], [107, 265], [107, 266], [107, 267], [107, 268], [107, 269], [107, 270], [107, 271], [107, 272], [107, 273], [107, 274], [108, 274], [108, 275], [108, 276], [108, 277], [108, 278], [108, 279], [108, 280], [108, 281], [108, 282], [108, 283], [108, 284], [108, 285], [108, 286], [108, 287], [108, 288], [108, 288], [107, 289], [107, 290], [107, 290], [106, 291], [106, 291], [105, 291], [104, 292], [104, 292]]]}, {'type': 'blood_vessel', 'coordinates': [[[505, 442], [504, 442], [503, 442], [502, 442], [501, 442], [500, 442], [499, 442], [499, 441], [498, 441], [497, 441], [497, 440], [496, 440], [496, 439], [495, 439], [495, 438], [494, 438], [494, 437], [493, 437], [493, 436], [493, 435], [492, 435], [492, 434], [492, 433], [491, 433], [491, 432], [491, 431], [491, 430], [490, 430], [490, 429], [490, 428], [490, 427], [490, 426], [490, 425], [490, 424], [489, 424], [489, 423], [489, 422], [489, 421], [489, 420], [489, 419], [489, 418], [489, 417], [489, 416], [489, 415], [489, 414], [490, 413], [490, 413], [490, 412], [490, 411], [491, 410], [491, 410], [491, 409], [491, 408], [492, 407], [492, 407], [492, 406], [493, 405], [493, 405], [493, 404], [494, 403], [494, 403], [495, 402], [495, 402], [495, 401], [496, 400], [496, 400], [496, 399], [497, 398], [497, 398], [497, 397], [498, 396], [498, 396], [499, 395], [499, 395], [500, 394], [501, 394], [501, 394], [502, 393], [502, 393], [503, 392], [503, 392], [504, 391], [505, 391], [505, 391], [506, 390], [507, 390], [507, 390], [508, 389], [509, 389], [510, 389], [511, 389], [511, 389], [511, 390], [511, 391], [511, 392], [511, 393], [511, 394], [511, 395], [511, 396], [511, 397], [511, 398], [511, 399], [511, 400], [511, 401], [511, 402], [511, 403], [511, 404], [511, 405], [511, 406], [511, 407], [511, 408], [511, 409], [511, 410], [511, 411], [511, 412], [511, 413], [511, 414], [511, 415], [511, 416], [511, 417], [511, 418], [511, 419], [511, 420], [511, 421], [511, 422], [511, 423], [511, 424], [511, 425], [511, 426], [511, 427], [511, 428], [511, 429], [511, 430], [511, 431], [511, 432], [511, 433], [511, 434], [511, 435], [511, 436], [511, 437], [511, 438], [511, 439], [511, 440], [511, 441], [511, 441], [511, 441], [510, 441], [509, 441], [508, 441], [507, 441], [506, 441], [505, 442], [505, 442]]]}, {'type': 'blood_vessel', 'coordinates': [[[375, 477], [374, 477], [373, 477], [372, 477], [371, 477], [370, 477], [369, 477], [369, 476], [368, 476], [368, 475], [367, 475], [367, 474], [366, 474], [366, 473], [365, 473], [365, 472], [364, 472], [364, 471], [363, 471], [363, 470], [362, 470], [362, 469], [362, 468], [361, 468], [360, 468], [360, 467], [359, 467], [359, 466], [359, 465], [358, 465], [357, 465], [357, 464], [356, 464], [355, 464], [354, 464], [354, 463], [353, 463], [353, 462], [352, 462], [351, 462], [351, 461], [350, 461], [350, 460], [349, 460], [349, 459], [348, 459], [347, 459], [346, 459], [346, 458], [345, 458], [345, 457], [344, 457], [343, 457], [343, 456], [342, 456], [342, 455], [341, 455], [340, 455], [340, 454], [339, 454], [338, 454], [337, 454], [337, 453], [336, 453], [336, 452], [335, 452], [335, 451], [334, 451], [334, 450], [333, 450], [333, 449], [333, 448], [332, 448], [331, 448], [331, 447], [330, 447], [329, 447], [329, 446], [328, 446], [328, 445], [328, 444], [327, 444], [327, 443], [326, 443], [326, 442], [326, 441], [326, 440], [326, 439], [327, 438], [327, 438], [328, 437], [329, 437], [330, 437], [330, 437], [331, 436], [331, 436], [332, 435], [333, 435], [334, 435], [335, 435], [336, 435], [336, 436], [337, 436], [338, 436], [339, 436], [340, 436], [341, 436], [341, 436], [342, 435], [343, 435], [343, 435], [344, 434], [345, 434], [346, 434], [347, 434], [348, 434], [349, 434], [350, 434], [351, 434], [352, 434], [353, 434], [353, 435], [354, 435], [354, 436], [355, 436], [355, 437], [356, 437], [356, 438], [357, 438], [357, 439], [358, 439], [358, 440], [359, 440], [359, 441], [360, 441], [360, 442], [361, 442], [361, 443], [361, 444], [362, 444], [362, 445], [363, 445], [363, 446], [363, 447], [364, 447], [364, 448], [364, 449], [365, 449], [365, 450], [365, 451], [365, 452], [366, 452], [366, 453], [366, 454], [367, 454], [367, 455], [367, 456], [367, 457], [368, 457], [368, 458], [369, 458], [369, 459], [369, 460], [370, 460], [370, 461], [370, 462], [370, 463], [371, 463], [371, 464], [372, 464], [372, 465], [373, 465], [373, 466], [373, 467], [374, 467], [374, 468], [375, 468], [375, 469], [375, 470], [375, 471], [376, 471], [376, 472], [376, 473], [376, 474], [376, 475], [376, 476], [376, 476], [375, 477], [375, 477]]]}, {'type': 'blood_vessel', 'coordinates': [[[368, 410], [367, 410], [366, 410], [365, 410], [364, 410], [364, 409], [363, 409], [362, 409], [362, 408], [361, 408], [361, 407], [361, 406], [360, 406], [360, 405], [359, 405], [358, 405], [357, 405], [357, 404], [356, 404], [355, 404], [355, 403], [354, 403], [353, 403], [352, 403], [351, 403], [351, 402], [350, 402], [349, 402], [349, 401], [348, 401], [348, 400], [347, 400], [347, 399], [346, 399], [345, 399], [345, 398], [344, 398], [344, 397], [344, 396], [343, 396], [343, 395], [343, 394], [342, 394], [342, 393], [342, 392], [342, 391], [342, 390], [341, 390], [341, 389], [341, 388], [340, 388], [340, 387], [340, 386], [340, 385], [340, 384], [339, 384], [339, 383], [339, 382], [339, 381], [339, 380], [338, 380], [338, 379], [338, 378], [338, 377], [337, 377], [337, 376], [337, 375], [337, 374], [337, 373], [336, 373], [336, 372], [336, 371], [336, 370], [336, 369], [336, 368], [335, 368], [335, 367], [335, 366], [335, 365], [335, 364], [335, 363], [335, 362], [335, 361], [335, 360], [335, 359], [336, 358], [336, 358], [336, 357], [336, 356], [337, 355], [337, 355], [337, 354], [338, 353], [339, 353], [339, 353], [340, 352], [341, 352], [342, 352], [343, 352], [344, 352], [345, 352], [346, 352], [346, 353], [347, 353], [348, 353], [348, 354], [349, 354], [350, 354], [351, 354], [351, 355], [352, 355], [353, 355], [354, 355], [354, 356], [355, 356], [356, 356], [356, 357], [357, 357], [357, 358], [358, 358], [358, 359], [359, 359], [360, 359], [361, 359], [361, 360], [362, 360], [363, 360], [364, 360], [364, 361], [365, 361], [366, 361], [366, 362], [367, 362], [367, 363], [368, 363], [369, 363], [370, 363], [370, 364], [371, 364], [372, 364], [372, 365], [373, 365], [373, 366], [374, 366], [375, 366], [375, 367], [376, 367], [376, 368], [377, 368], [377, 369], [378, 369], [379, 369], [379, 370], [380, 370], [380, 371], [381, 371], [381, 372], [382, 372], [382, 373], [383, 373], [383, 374], [383, 375], [384, 375], [384, 376], [384, 377], [384, 378], [384, 378], [383, 379], [383, 380], [383, 380], [382, 380], [381, 381], [381, 381], [380, 381], [379, 381], [378, 381], [377, 381], [377, 380], [376, 380], [375, 380], [375, 379], [374, 379], [373, 379], [372, 379], [371, 380], [371, 380], [370, 381], [370, 382], [370, 382], [369, 382], [368, 383], [368, 383], [367, 383], [366, 383], [365, 384], [365, 384], [364, 385], [364, 385], [363, 386], [363, 387], [363, 388], [363, 389], [364, 389], [364, 390], [364, 391], [365, 391], [365, 392], [366, 392], [366, 393], [367, 393], [367, 394], [368, 394], [368, 395], [368, 396], [369, 396], [369, 397], [369, 398], [370, 398], [370, 399], [370, 400], [370, 401], [370, 402], [370, 403], [370, 404], [371, 404], [371, 405], [371, 406], [371, 407], [371, 407], [370, 408], [370, 409], [370, 409], [369, 409], [368, 410], [368, 410]]]}, {'type': 'blood_vessel', 'coordinates': [[[339, 249], [338, 249], [337, 249], [336, 249], [335, 249], [334, 249], [333, 249], [332, 249], [332, 248], [331, 248], [330, 248], [329, 248], [329, 247], [328, 247], [327, 247], [326, 247], [325, 247], [324, 247], [323, 247], [322, 247], [322, 246], [321, 246], [320, 246], [320, 245], [319, 245], [319, 244], [318, 244], [318, 243], [317, 243], [316, 243], [316, 242], [315, 242], [315, 241], [314, 241], [314, 240], [313, 240], [313, 239], [313, 238], [312, 238], [312, 237], [312, 236], [311, 236], [311, 235], [310, 235], [310, 234], [310, 233], [309, 233], [309, 232], [309, 231], [308, 231], [308, 230], [307, 230], [307, 229], [307, 228], [307, 227], [307, 226], [306, 226], [306, 225], [305, 225], [305, 224], [305, 223], [305, 222], [305, 221], [304, 221], [304, 220], [304, 219], [303, 219], [303, 218], [302, 218], [302, 217], [301, 217], [301, 216], [301, 215], [301, 214], [300, 214], [300, 213], [300, 212], [299, 212], [299, 211], [298, 211], [298, 210], [298, 209], [297, 209], [297, 208], [297, 207], [297, 206], [297, 205], [297, 204], [297, 203], [297, 202], [298, 201], [298, 201], [298, 200], [299, 199], [299, 199], [300, 198], [301, 198], [302, 198], [303, 198], [304, 198], [304, 199], [305, 199], [306, 199], [307, 199], [307, 200], [308, 200], [309, 200], [309, 201], [310, 201], [310, 202], [311, 202], [312, 202], [312, 203], [313, 203], [314, 203], [314, 204], [315, 204], [316, 204], [316, 205], [317, 205], [318, 205], [318, 206], [319, 206], [319, 207], [320, 207], [320, 208], [321, 208], [321, 209], [322, 209], [322, 210], [323, 210], [323, 211], [323, 212], [324, 212], [324, 213], [325, 213], [325, 214], [325, 215], [326, 215], [326, 216], [327, 216], [327, 217], [328, 217], [328, 218], [328, 219], [329, 219], [329, 220], [329, 221], [330, 221], [330, 222], [330, 223], [331, 223], [331, 224], [331, 225], [332, 225], [332, 226], [332, 227], [333, 227], [333, 228], [333, 229], [333, 230], [334, 230], [334, 231], [334, 232], [334, 233], [335, 233], [335, 234], [335, 235], [336, 235], [336, 236], [336, 237], [336, 238], [337, 238], [337, 239], [337, 240], [338, 240], [338, 241], [338, 242], [339, 242], [339, 243], [340, 243], [340, 244], [340, 245], [340, 246], [340, 247], [340, 248], [340, 248], [339, 249], [339, 249]]]}, {'type': 'blood_vessel', 'coordinates': [[[352, 67], [351, 67], [350, 67], [349, 67], [348, 67], [347, 67], [346, 67], [345, 67], [345, 66], [344, 66], [343, 66], [343, 65], [343, 64], [342, 64], [342, 63], [342, 62], [343, 61], [343, 61], [343, 60], [344, 59], [345, 59], [345, 59], [345, 58], [345, 57], [346, 56], [346, 56], [346, 55], [346, 54], [346, 53], [347, 52], [347, 52], [347, 51], [347, 50], [348, 49], [348, 49], [348, 48], [349, 47], [349, 47], [350, 46], [350, 46], [351, 45], [351, 45], [351, 44], [352, 43], [352, 43], [352, 42], [353, 41], [353, 41], [353, 40], [354, 39], [354, 39], [354, 38], [355, 37], [355, 37], [356, 36], [356, 36], [356, 35], [357, 34], [357, 34], [358, 33], [358, 33], [359, 32], [360, 32], [360, 32], [361, 31], [362, 31], [363, 31], [364, 31], [365, 31], [365, 31], [366, 30], [367, 30], [368, 30], [368, 30], [369, 29], [370, 29], [370, 29], [371, 28], [372, 28], [373, 28], [373, 28], [374, 27], [374, 27], [375, 26], [376, 26], [376, 26], [377, 25], [378, 25], [379, 25], [380, 25], [380, 25], [381, 24], [381, 24], [381, 23], [382, 22], [382, 22], [383, 21], [383, 21], [384, 20], [384, 20], [385, 19], [385, 19], [385, 18], [385, 17], [385, 16], [385, 15], [385, 14], [385, 13], [386, 12], [386, 12], [386, 11], [387, 10], [388, 10], [388, 10], [389, 9], [390, 9], [391, 9], [391, 10], [392, 10], [393, 10], [393, 11], [394, 11], [394, 12], [394, 13], [395, 13], [396, 13], [396, 14], [396, 15], [397, 15], [398, 15], [398, 16], [399, 16], [400, 16], [401, 16], [401, 16], [402, 15], [403, 15], [404, 15], [405, 15], [406, 15], [407, 15], [408, 15], [409, 15], [410, 15], [411, 15], [411, 15], [412, 14], [412, 14], [413, 13], [414, 13], [414, 13], [415, 12], [416, 12], [416, 12], [417, 11], [418, 11], [419, 11], [420, 11], [421, 11], [422, 11], [423, 11], [423, 12], [424, 12], [425, 12], [425, 13], [425, 14], [426, 14], [426, 15], [426, 16], [426, 17], [426, 18], [426, 18], [425, 19], [425, 20], [425, 20], [424, 21], [424, 21], [423, 22], [423, 23], [423, 23], [422, 24], [422, 25], [422, 25], [421, 26], [421, 26], [420, 27], [420, 27], [419, 28], [419, 28], [418, 29], [418, 29], [417, 30], [417, 30], [416, 30], [415, 31], [415, 31], [414, 31], [413, 32], [413, 32], [412, 33], [412, 33], [411, 33], [410, 34], [410, 34], [409, 34], [408, 34], [407, 35], [407, 35], [406, 35], [405, 36], [405, 36], [404, 36], [403, 37], [403, 37], [402, 37], [401, 37], [400, 37], [399, 38], [399, 38], [398, 38], [397, 39], [397, 39], [396, 39], [395, 39], [394, 39], [393, 39], [392, 40], [392, 40], [391, 40], [390, 40], [389, 41], [389, 41], [388, 41], [387, 41], [386, 41], [385, 42], [385, 42], [384, 42], [383, 42], [382, 43], [382, 43], [381, 44], [381, 44], [380, 45], [380, 45], [379, 45], [378, 46], [378, 46], [377, 47], [377, 47], [376, 48], [376, 48], [375, 49], [375, 49], [374, 50], [374, 50], [373, 51], [373, 52], [373, 52], [372, 53], [372, 53], [371, 53], [370, 54], [370, 55], [370, 55], [369, 55], [368, 56], [368, 57], [368, 57], [367, 58], [367, 59], [367, 59], [366, 60], [366, 60], [365, 61], [365, 61], [364, 61], [363, 62], [363, 62], [362, 62], [361, 62], [360, 63], [360, 63], [359, 63], [358, 64], [358, 64], [357, 65], [357, 65], [356, 65], [355, 66], [355, 66], [354, 66], [353, 66], [352, 67], [352, 67]]]}, {'type': 'blood_vessel', 'coordinates': [[[227, 299], [226, 299], [225, 299], [224, 299], [223, 299], [222, 299], [221, 299], [221, 298], [220, 298], [219, 298], [219, 297], [218, 297], [218, 296], [217, 296], [216, 296], [216, 295], [215, 295], [215, 294], [214, 294], [213, 294], [213, 293], [212, 293], [211, 293], [211, 292], [210, 292], [210, 291], [209, 291], [209, 290], [208, 290], [207, 290], [207, 289], [206, 289], [206, 288], [205, 288], [205, 287], [204, 287], [203, 287], [203, 286], [202, 286], [202, 285], [201, 285], [201, 284], [200, 284], [200, 283], [199, 283], [198, 283], [198, 282], [197, 282], [197, 281], [196, 281], [195, 281], [195, 280], [194, 280], [194, 279], [194, 278], [193, 278], [193, 277], [192, 277], [192, 276], [191, 276], [191, 275], [190, 275], [190, 274], [189, 274], [189, 273], [188, 273], [187, 273], [186, 273], [185, 273], [185, 272], [184, 272], [183, 272], [182, 272], [182, 271], [181, 271], [180, 271], [180, 270], [179, 270], [178, 270], [178, 269], [177, 269], [177, 268], [176, 268], [176, 267], [176, 266], [175, 266], [175, 265], [175, 264], [175, 263], [174, 263], [174, 262], [174, 261], [175, 260], [175, 260], [175, 259], [176, 258], [176, 258], [177, 257], [178, 257], [178, 257], [178, 256], [179, 255], [179, 255], [180, 254], [181, 254], [181, 254], [182, 253], [183, 253], [183, 253], [184, 252], [185, 252], [185, 252], [186, 251], [187, 251], [187, 251], [188, 250], [189, 250], [189, 250], [190, 249], [190, 249], [191, 248], [191, 248], [192, 247], [192, 247], [193, 246], [193, 246], [194, 245], [194, 245], [195, 244], [196, 244], [196, 244], [197, 243], [198, 243], [198, 243], [199, 242], [200, 242], [200, 242], [201, 241], [202, 241], [203, 241], [203, 241], [204, 240], [205, 240], [206, 240], [207, 240], [208, 240], [209, 240], [210, 240], [211, 240], [212, 240], [213, 240], [214, 240], [215, 240], [216, 240], [217, 240], [218, 240], [219, 240], [220, 240], [220, 241], [221, 241], [222, 241], [222, 242], [223, 242], [223, 243], [224, 243], [224, 244], [225, 244], [225, 245], [225, 246], [226, 246], [226, 247], [226, 248], [226, 249], [226, 250], [226, 251], [226, 252], [226, 253], [226, 254], [226, 255], [226, 256], [226, 257], [226, 258], [226, 259], [226, 260], [226, 261], [227, 261], [227, 262], [227, 263], [227, 264], [227, 265], [227, 266], [227, 267], [227, 268], [228, 268], [228, 269], [228, 270], [228, 271], [228, 272], [228, 273], [228, 274], [228, 275], [229, 275], [229, 276], [229, 277], [229, 278], [229, 279], [230, 279], [230, 280], [230, 281], [231, 281], [231, 282], [231, 283], [231, 284], [231, 285], [232, 285], [232, 286], [232, 287], [232, 288], [232, 289], [232, 290], [232, 291], [232, 292], [232, 293], [232, 294], [232, 294], [231, 295], [231, 296], [231, 296], [230, 297], [230, 297], [229, 298], [229, 298], [228, 298], [227, 299], [227, 299]]]}]}]

Al analizar la primera línea de la máscara asociada a una imagen, observamos que esta incluye el ID de la imagen, lo que facilita su vinculación y visualización posterior. Además, contiene las anotaciones que identifican y delimitan las regiones de interés, permitiendo dibujar los píxeles segmentados y resaltar las estructuras específicas dentro del tejido. Este proceso es particularmente interesante, ya que posibilita una mejor interpretación de las características morfológicas y patológicas presentes en la muestra, contribuyendo así a un análisis más preciso y detallado.

data_info_mascaras = pd.DataFrame(mascaras)
data_info_mascaras.head()
id annotations
0 0006ff2aa7cd [{'type': 'glomerulus', 'coordinates': [[[167,...
1 00168d1b7522 [{'type': 'glomerulus', 'coordinates': [[[511,...
2 0033bbc76b6b [{'type': 'blood_vessel', 'coordinates': [[[16...
3 003504460b3a [{'type': 'blood_vessel', 'coordinates': [[[40...
4 004daf1cbe75 [{'type': 'blood_vessel', 'coordinates': [[[14...
data_info_mascaras["id"].unique(). shape[0]
1633

Importante: Nótese que todas las máscaras tienen un id único. Esto coincide con la cantidad de imágenes de teselas que se obtuvieron de los WSI.

Unión de tablas#

Dado que tanto las máscaras como las imágenes tienen la misma cantidad de ID únicos (1633), podemos realizar un merge entre las tablas data_info_mascaras y data_info_teselas. Esto nos permitirá verificar si los ID coinciden correctamente y asegurarnos de que cada imagen tiene su máscara correspondiente. De esta manera, podremos evaluar la integridad de los datos y confirmar que todas las imágenes cuentan con su respectiva segmentación.

data_imagenes = pd.merge(data_info_mascaras, data_info_teselas, on="id", how="inner")
data_imagenes.head()
id annotations source_wsi dataset i j
0 0006ff2aa7cd [{'type': 'glomerulus', 'coordinates': [[[167,... 2 2 16896 16420
1 00168d1b7522 [{'type': 'glomerulus', 'coordinates': [[[511,... 2 2 14848 14884
2 0033bbc76b6b [{'type': 'blood_vessel', 'coordinates': [[[16... 1 1 10240 43008
3 003504460b3a [{'type': 'blood_vessel', 'coordinates': [[[40... 3 2 8192 11776
4 004daf1cbe75 [{'type': 'blood_vessel', 'coordinates': [[[14... 3 2 6144 11264
data_imagenes["id"].unique(). shape[0]
1633

En efecto, sí se cumple que cada imagen tenga su máscara asociada y coincidan.

Visualización de imágenes#

Veamos las primeras 5 imágenes del conjunto de datos

mostrar_imagenes(data_info_teselas, ruta_imagenes, 5)
_images/10677b005ecad7a843e6d45225d00fb974ca389f81cce414f2328252c0aab019.png

La visualización muestra las primeras 5 imágenes del conjunto de datos de entrenamiento, identificadas por su ID único. Se trata de imágenes histológicas donde se pueden apreciar diferentes estructuras celulares y tisulares.

Ahora veamos la máscara de la primera imagen.

graficar_imagen_mascara(mascaras[0])

La estructura de las máscaras resulta especialmente interesante, ya que no solo permiten visualizar las anotaciones, sino que también facilitan la superposición con la imagen original. Esta forma de representación gráfica es clave para analizar y comprender mejor las regiones de interés, permitiendo una interpretación más clara de las segmentaciones y su correspondencia con la imagen real.

# Crear máscaras vacías para cada tipo de anotación
def crear_mascaras_vacias(ANCHO_IMAGEN=512, ALTO_IMAGEN=512, NUMERO_CANAL=3):
    """Genera máscaras vacías para cada tipo de estructura anatómica anotada en la imagen."""
    mascara_glomerulo = np.zeros((ANCHO_IMAGEN, ALTO_IMAGEN, NUMERO_CANAL), dtype=np.uint8)
    mascara_vaso_sanguineo = np.zeros((ANCHO_IMAGEN, ALTO_IMAGEN, NUMERO_CANAL), dtype=np.uint8)
    mascara_indefinida = np.zeros((ANCHO_IMAGEN, ALTO_IMAGEN, NUMERO_CANAL), dtype=np.uint8)
    return {'glomerulus': mascara_glomerulo, 'blood_vessel': mascara_vaso_sanguineo, 'unsure': mascara_indefinida}

# Rellenar las máscaras con las anotaciones de la imagen
def rellenar_mascaras_con_anotaciones(id_imagen, mascaras):
    """Dibuja las regiones anotadas en las máscaras correspondientes usando coordenadas predefinidas."""
    anotaciones = data_imagenes.loc[data_imagenes["id"] == id_imagen, 'annotations'].iloc[0]
    
    for anotacion in anotaciones:
        tipo_anotacion = anotacion['type']  # Tipo de estructura anotada
        coordenadas = anotacion['coordinates']  # Lista de coordenadas del polígono anotado
        color = np.random.randint(0, 255, size=3).tolist()  # Genera un color aleatorio para cada instancia
        
        # Rellena la región anotada en la máscara correspondiente
        cv2.fillPoly(mascaras[tipo_anotacion], [np.array(coordenadas)], color)
    
    return mascaras
# Graficar las máscaras generadas
def graficar_mascaras(id_imagen):
    """Muestra las máscaras generadas para cada tipo de anotación."""
    mascaras_vacias = crear_mascaras_vacias()
    mascaras = rellenar_mascaras_con_anotaciones(id_imagen, mascaras_vacias)
    
    fig, axs = plt.subplots(1, 3, figsize=(8, 8))
    axs[0].imshow(mascaras['glomerulus'], cmap='gray')
    axs[0].set_title('Máscara Glomérulo')

    axs[1].imshow(mascaras['blood_vessel'], cmap='gray')
    axs[1].set_title('Máscara Vaso Sanguíneo')

    axs[2].imshow(mascaras['unsure'], cmap='gray')
    axs[2].set_title('Máscara Indefinida')
    
    plt.tight_layout()
    plt.show()

# Combinar todas las máscaras en una sola imagen
def combinar_mascaras(mascaras):
    """Concatena todas las máscaras en un solo array de imágenes para visualización."""
    mascara_glomerulo = mascaras['glomerulus']
    mascara_vaso_sanguineo = mascaras['blood_vessel']
    mascara_indefinida = mascaras['unsure']
    
    # Se concatenan a lo largo del tercer eje (canal de color)
    imagen_combinada = np.concatenate([mascara_glomerulo, mascara_vaso_sanguineo, mascara_indefinida], axis=2)
    return imagen_combinada
# Obtener una imagen a partir de su ID
def obtener_imagen(id_imagen):
    """Carga la imagen original desde la ruta especificada."""
    ruta_imagen = ruta_imagenes + id_imagen + ".tif"
    imagen = imageio.v2.imread(ruta_imagen)
    return imagen
# Obtener imagen con máscaras superpuestas
def obtener_imagen_con_mascaras_superpuestas(id_imagen):
    """Superpone las máscaras sobre la imagen original para visualización."""
    anotaciones = data_imagenes.loc[data_imagenes["id"] == id_imagen, 'annotations'].iloc[0]
    img = obtener_imagen(id_imagen)
    
    # Definir colores específicos para cada tipo de anotación
    ROJO = (255, 0, 0)         # Para vasos sanguíneos
    VERDE_OSCURO = (0, 255, 0) # Para glomérulos
    AZUL_OSCURO = (0, 0, 255)  # Para áreas indefinidas
    
    mapa_colores = {'blood_vessel': ROJO, 'glomerulus': VERDE_OSCURO, 'unsure': AZUL_OSCURO}
    conteo_instancias = {'blood_vessel': 0, 'glomerulus': 0, 'unsure': 0}
    
    for anotacion in anotaciones:
        color = mapa_colores[anotacion['type']]
        conteo_instancias[anotacion['type']] += 1
        coords = np.array(anotacion['coordinates'])
        
        # Dibuja los contornos de las anotaciones sobre la imagen
        cv2.polylines(img, coords, True, color, 3)
    
    return img, conteo_instancias

# Dibujar la imagen con la superposición de máscaras
def dibujar_mascaras_sobre_imagen(id_imagen):
    """Muestra la imagen con las máscaras superpuestas y cuenta las instancias de cada anotación."""
    img, conteo_instancias = obtener_imagen_con_mascaras_superpuestas(id_imagen)
    
    print(f"Vasos Sanguíneos Conteo: {conteo_instancias['blood_vessel']}")
    print(f"Glomérulos Conteo: {conteo_instancias['glomerulus']}")
    print(f"Indefinido Conteo: {conteo_instancias['unsure']}")
# Graficar las máscaras y la imagen original
def graficar_mascaras_y_imagen_original(id_imagen):
    """Dibuja la imagen original junto con las máscaras generadas y su superposición."""
    mascaras_vacias = crear_mascaras_vacias()
    mascaras = rellenar_mascaras_con_anotaciones(id_imagen, mascaras_vacias)
    imagen = obtener_imagen(id_imagen)
    imagen_superpuesta, _ = obtener_imagen_con_mascaras_superpuestas(id_imagen)
    
    fig, axs = plt.subplots(1, 5, figsize=(16, 16))
    axs[0].imshow(mascaras['glomerulus'], cmap='gray')
    axs[0].set_title('Máscara Glomérulo')

    axs[1].imshow(mascaras['blood_vessel'], cmap='gray')
    axs[1].set_title('Máscara Vaso Sanguíneo')

    axs[2].imshow(mascaras['unsure'], cmap='gray')
    axs[2].set_title('Máscara Indefinida')
    
    axs[3].imshow(imagen)
    axs[3].set_title('Imagen Original')
    
    axs[4].imshow(imagen_superpuesta)
    axs[4].set_title("Máscara sobre la Imagen")
    
    # Quitar los ejes para mejor visualización
    for ax in axs.flat:
        ax.set_xticks([])
        ax.set_yticks([])
        
    plt.tight_layout()
    plt.show()
graficar_mascaras_y_imagen_original(imagen_pruebas)
_images/f9c8a4f853166814c4ff52fc89e75694227e01b3f14c5b70a9a9f8fe327aef1c.png
dibujar_mascaras_sobre_imagen(imagen_pruebas)
Vasos Sanguíneos Conteo: 8
Glomérulos Conteo: 1
Indefinido Conteo: 0

Podemos notar que en las imágenes hay un total de 8 máscaras asociadas a la estructura de vasos sanguíneos (blood vessels) y 1 máscara correspondiente a un glomérulo. No se identificaron máscaras en la categoría indefinida. En la visualización, las máscaras segmentadas se muestran en distintos colores en los primeros paneles, mientras que en la última imagen se superponen sobre la muestra original, donde los vasos sanguíneos están delineados en rojo y el glomérulo en verde. Esto indica que el proceso de segmentación está diferenciando correctamente estas estructuras dentro del tejido.

graficar_mascaras_y_imagen_original(imagen_pruebas2)
_images/156130155db6a353d5b691d7d46fd67190dc407dbb4373b8f7eae440f1357e74.png
dibujar_mascaras_sobre_imagen(imagen_pruebas2)
Vasos Sanguíneos Conteo: 51
Glomérulos Conteo: 0
Indefinido Conteo: 2

Podemos notar que en las imágenes hay un total de 51 máscaras asociadas a vasos sanguíneos, mientras que no se identificaron glomérulos y se detectaron 2 regiones dentro de la categoría indefinida. En la visualización, los vasos sanguíneos segmentados se muestran en distintos colores en la segunda imagen, mientras que en la última imagen las máscaras se superponen sobre la muestra original, con los vasos sanguíneos delineados en rojo y las regiones indefinidas en azul. Esto indica que el modelo ha identificado una gran cantidad de estructuras vasculares en la muestra, pero no ha detectado glomérulos en esta región específica del tejido.

Análisis descriptivo de imágenes#

En esta sección veremos un poco más sobre cómo están distribuidas las clases, los píxeles y colores del conjunto de imágenes.

¿Qué son los píxeles?

Los píxeles son los puntos que forman una imagen digital en una pantalla. Son la unidad mínima de información gráfica que se muestra en una computadora.

Distribución de anotaciones#

# Expandir la columna 'annotations' para obtener cada anotación individualmente
annotations_list = data_imagenes['annotations'].explode()

# Extraer el campo 'type' de cada diccionario dentro de las anotaciones
types = annotations_list.apply(lambda x: x['type'])

# Contar la cantidad de ocurrencias de cada tipo de anotación
type_counts = types.value_counts()

colores_morados = ["#6A0DAD",  "#9932CC", "#BA55D3"]

plt.pie(type_counts, 
        labels=type_counts.index, 
        autopct=lambda pct: f'{pct:.1f}% ({int(pct * sum(type_counts)/100)})', 
        colors=colores_morados)

plt.title("Distribución de Anotaciones por Tipo")
plt.show()
plt.close()
_images/7794a0a1f47f27dd2110bdf80af83a3d7f100dbd2f8a96353e3476e271a81c5a.png

Tamaño de las imágenes#

Tamaño de las imágenes

El tamaño de una imagen en píxeles se expresa con dos números: el número de columnas de píxeles (ancho) y el número de filas de píxeles (alto). (Tamaño = ancho * alto)

El conjunto de imágenes proporcionado tiene un tamaño uniforme de 512x512 píxeles, ya que fueron previamente procesadas. Esto se debe a que cada imagen es un fragmento extraído de una WSI (Whole Slide Image), que ha sido segmentada en porciones más pequeñas para facilitar su análisis y procesamiento.

imagen = obtener_imagen(imagen_pruebas)
imagen.shape
(512, 512, 3)

En efecto, las imágenes del conjunto de datos tienen un tamaño de 512x512 píxeles y cuentan con 3 canales de color, lo que indica que son imágenes en formato RGB. Este formato es estándar en procesamiento de imágenes médicas y permite realizar análisis detallados de la estructura y composición del tejido.

Mapa de intensidad de píxeles#

Para comprender mejor la distribución de los valores de los píxeles en nuestras imágenes, visualizaremos un mapa de intensidad. Este tipo de gráfico nos permite identificar patrones en la luminosidad y contraste, resaltando áreas de mayor y menor intensidad en una imagen. Convertiremos la imagen a escala de grises si es necesario y aplicaremos un heatmap para representar la variación en los niveles de intensidad de los píxeles. Esto es útil para detectar características relevantes en el procesamiento y análisis de imágenes.

Colores en una imagen

En una imagen en escala de grises:

  • Si el valor del píxel se acerca a 0, significa que es negro (más oscuro).

  • Si el valor del píxel se acerca a 255, significa que es blanco (más claro).

En imágenes en color (RGB), cada canal (Rojo, Verde, Azul) sigue la misma lógica:

  • 0 en un canal = ausencia de ese color.

  • 255 en un canal = intensidad máxima de ese color.

def generar_mapa_intensidad(id_imagen, ruta_imagenes):
    ruta_imagen = os.path.join(ruta_imagenes, id_imagen + ".tif")
    imagen = cv2.imread(ruta_imagen, cv2.IMREAD_GRAYSCALE)

    if imagen is None:
        print(f"No se pudo cargar la imagen {id_imagen}")
        return

    histograma, bins = np.histogram(imagen.ravel(), bins=256, range=[0, 256])

    fig, ax = plt.subplots(1, 2, figsize=(12, 6))

    # Mostrar el mapa de intensidad
    sns.heatmap(imagen, cmap="Purples", cbar=True, ax=ax[0])
    ax[0].set_title(f"Mapa de Intensidad - {id_imagen}")
    ax[0].axis("off")

    # Mostrar el histograma de intensidades
    ax[1].bar(range(256), histograma, color='purple', alpha=0.7)
    ax[1].set_title("Distribución de Intensidad de Píxeles")
    ax[1].set_xlabel("Intensidad de píxeles (0-255)")
    ax[1].set_ylabel("Frecuencia")

    plt.tight_layout()
    plt.show()

generar_mapa_intensidad(imagen_pruebas, ruta_imagenes)
_images/f147eafd96e144cd253218dde38039490d6f30504cb620004308032432d7db2f.png

La imagen presenta un mapa de intensidad de píxeles y su correspondiente histograma para la imagen identificada como “0006ff2aa7cd”. En el mapa de intensidad, los tonos más oscuros representan regiones de mayor concentración de píxeles de baja intensidad, mientras que los tonos más claros indican áreas con valores más altos. A la derecha, el histograma muestra la distribución de los niveles de intensidad en un rango de 0 a 255, con una mayor frecuencia de píxeles en intensidades intermedias (alrededor de 100-150), lo que sugiere una predominancia de tonos grises en la imagen. Este análisis es útil para evaluar el contraste y la estructura de la imagen en estudios de diagnóstico o procesamiento de imágenes médicas.

Histograma de escala de grises#

def mostrar_histograma_escala_grises(id_imagen, ruta_imagenes):

    imagen = imageio.imread(os.path.join(ruta_imagenes, id_imagen + ".tif"))
    imagen_gris = cv2.cvtColor(imagen, cv2.COLOR_RGB2GRAY)
    histograma, bins = np.histogram(imagen_gris.ravel(), bins=256, range=[0,256])
    fig, axs = plt.subplots(1, 3, figsize=(12,4))
    
    # Imagen original
    axs[0].imshow(imagen)
    axs[0].set_title("Imagen Original")
    axs[0].axis("off")

    # Imagen en escala de grises
    axs[1].imshow(imagen_gris, cmap="gray")
    axs[1].set_title("Imagen en Escala de Grises")
    axs[1].axis("off")

    # Histograma de intensidades con barras más gruesas
    axs[2].bar(range(256), histograma, color="purple", alpha=1.0, width=1.5)
    axs[2].set_xlim([0, 255])  # Asegura que cubra todo el rango de intensidades
    axs[2].set_xlabel("Intensidad de píxeles (0-255)")
    axs[2].set_ylabel("Frecuencia")
    axs[2].set_title("Histograma de Escala de Grises")
    axs[2].grid(False)  # Desactiva la cuadrícula para más claridad

    plt.tight_layout()
    plt.show()


mostrar_histograma_escala_grises(imagen_pruebas, ruta_imagenes)
_images/f8e85e5e911f3c71a52366401906950a26ada189f844fa8235df17a40695f511.png

La imagen muestra un análisis de intensidad de píxeles en una muestra de tejido. A la izquierda, la imagen original conserva su coloración característica, mientras que en el centro se presenta su versión en escala de grises, resaltando contrastes estructurales. A la derecha, el histograma de escala de grises representa la distribución de intensidades de píxeles (0-255), con un pico en valores intermedios, indicando que predominan tonos grises de intensidad media. La alta frecuencia de ciertos valores sugiere regiones con estructuras bien definidas dentro del tejido, lo que puede ser útil para su segmentación.

Distribución de canales de color#

ruta_imagen = os.path.join(ruta_imagenes.strip(), imagen_pruebas + ".tif")

imagen = cv2.imread(ruta_imagen)
imagen = cv2.cvtColor(imagen, cv2.COLOR_BGR2RGB) 
R, G, B = imagen[:, :, 0], imagen[:, :, 1], imagen[:, :, 2]

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Mostrar la imagen original
axes[0, 0].imshow(imagen)
axes[0, 0].axis("off")
axes[0, 0].set_title("Imagen Original")

# Crear los histogramas de cada canal
sns.histplot(R.flatten(), color="red", ax=axes[0, 1], kde=True, bins=50)
axes[0, 1].set_title("Distribución de Intensidad - Rojo")
axes[0, 1].set_xlabel("Intensidad de píxeles")
axes[0, 1].set_ylabel("Frecuencia")

sns.histplot(G.flatten(), color="green", ax=axes[1, 0], kde=True, bins=50)
axes[1, 0].set_title("Distribución de Intensidad - Verde")
axes[1, 0].set_xlabel("Intensidad de píxeles")
axes[1, 0].set_ylabel("Frecuencia")

sns.histplot(B.flatten(), color="blue", ax=axes[1, 1], kde=True, bins=50)
axes[1, 1].set_title("Distribución de Intensidad - Azul")
axes[1, 1].set_xlabel("Intensidad de píxeles")
axes[1, 1].set_ylabel("Frecuencia")

plt.tight_layout()  
plt.show()
_images/74597bfa24c613ef7af74168597c0bbab16536d308fb4e1558ef68526cfc098f.png

La imagen original en la esquina superior izquierda muestra una micrografía teñida predominantemente en tonos morados. Los histogramas de los canales de color rojo, verde y azul muestran la distribución de intensidad de píxeles en cada componente. Se observa que el canal azul tiene valores más altos de intensidad en comparación con los otros canales, lo que indica una predominancia de este color en la imagen. El canal rojo tiene una distribución bimodal con un pico principal alrededor de 150 y otro menor cerca de 200, mientras que el canal verde muestra un comportamiento similar pero con su pico principal más desplazado hacia la izquierda. Estas distribuciones reflejan la composición cromática de la imagen y pueden ser útiles en análisis de segmentación.

Filtrado de imágenes con OpenCV#

A continuación se hará un filtrado de imágenes.

¿Qué son los filtros en imágenes?

Los filtros en imágenes son operaciones matemáticas aplicadas sobre los píxeles para modificar ciertas características visuales. Estos pueden utilizarse para mejorar el contraste, eliminar ruido o detectar bordes. En este caso, se emplean filtros de suavizado para reducir el ruido presente en la imagen original.

Importancia del Filtrado de Imágenes

El filtrado de imágenes es un proceso fundamental en la visión por computadora, ya que permite mejorar la calidad de las imágenes eliminando ruido, resaltando características importantes y suavizando detalles no deseados.

Note

Tipos de filtros aplicados

  • Filtro de Media (cv2.blur): Calcula el promedio de los valores de píxeles en una vecindad definida por una ventana (en este caso, de 3x3). Suaviza la imagen pero puede afectar los bordes.

  • Filtro de Mediana (cv2.medianBlur): Sustituye el valor de cada píxel por la mediana de los valores en su vecindad. Es especialmente útil para eliminar ruido impulsivo o “ruido sal y pimienta” (píxeles blancos (sal) y píxeles negros (pimienta))

  • Filtro Gaussiano (cv2.GaussianBlur): Aplica un suavizado utilizando una función gaussiana, lo que genera un efecto más natural y mantiene mejor los bordes en comparación con el filtro de media.

def aplicar_filtros_y_visualizar(imagen):
    # Aplicación de filtros
    meanimg = cv2.blur(imagen, (3, 3))  # Filtro de media
    medianimg = cv2.medianBlur(imagen, 3)  # Filtro de mediana
    gaussianimg = cv2.GaussianBlur(imagen, (3, 3), 0)  # Filtro gaussiano

    # Creación de la figura con subgráficos 2x2
    fig, axs = plt.subplots(2, 2, figsize=(10, 10))

    # Imagen Original con Ruido
    axs[0, 0].imshow(cv2.cvtColor(imagen, cv2.COLOR_BGR2RGB))
    axs[0, 0].set_title('Imagen con Ruido', fontsize=14)
    axs[0, 0].axis('off')

    # Filtro Media
    axs[0, 1].imshow(cv2.cvtColor(meanimg, cv2.COLOR_BGR2RGB))
    axs[0, 1].set_title('Filtro Media', fontsize=14)
    axs[0, 1].axis('off')

    # Filtro Mediana
    axs[1, 0].imshow(cv2.cvtColor(medianimg, cv2.COLOR_BGR2RGB))
    axs[1, 0].set_title('Filtro Mediana', fontsize=14)
    axs[1, 0].axis('off')

    # Filtro Gaussiano
    axs[1, 1].imshow(cv2.cvtColor(gaussianimg, cv2.COLOR_BGR2RGB))
    axs[1, 1].set_title('Filtro Gaussiano', fontsize=14)
    axs[1, 1].axis('off')

    # Ajustar diseño y mostrar
    plt.tight_layout()
    plt.show()
ruta_imagen = os.path.join(ruta_imagenes.strip(), imagen_pruebas + ".tif")
imagen = cv2.imread(ruta_imagen)
aplicar_filtros_y_visualizar(imagen)
_images/cf6b614c05b15a2e5b6966be0ef53c5cf5c64a8cfdecc54fe55ffcd1668b0c6a.png

La imagen muestra el efecto de diferentes filtros de suavizado aplicados a una imagen microscópica teñida en tonos morados. En la esquina superior izquierda se observa la imagen original con ruido, donde las estructuras celulares presentan detalles nítidos pero también cierta irregularidad en la distribución de los píxeles. En la parte superior derecha, el filtro de media suaviza la imagen al promediar los valores de los píxeles vecinos, reduciendo el ruido pero a costa de difuminar algunos detalles importantes. En la esquina inferior izquierda, el filtro de mediana elimina eficazmente el ruido tipo “sal y pimienta”, preservando mejor los bordes y manteniendo la claridad de las estructuras celulares. Finalmente, en la parte inferior derecha, el filtro gaussiano aplica una suavización más natural, logrando un equilibrio entre la reducción de ruido y la conservación de los detalles de la imagen. Estos filtros son útiles en el preprocesamiento de imágenes biomédicas, mejorando su calidad visual y facilitando su análisis.

Veamos la aplicación de los filtros para otra observación.

ruta_imagen2 = os.path.join(ruta_imagenes.strip(), imagen_pruebas2 + ".tif")
imagen2 = cv2.imread(ruta_imagen2)
aplicar_filtros_y_visualizar(imagen2)
_images/e389e63dacc7f65a546c80b6d30344ee54f6920d1965316b39633cbd0effa8dd.png

Referencias#

[1]

Addison Howard, Katherine Gustilo, Katy Borner, Ryan Holbrook, and Yashvardhan Jain. Hubmap - hacking the human vasculature. https://kaggle.com/competitions/hubmap-hacking-the-human-vasculature, 2023. Kaggle.